Github
Poststhree.jsOptimizing Many Objects

Optimizing Many Objects

draw call 오버헤드와 geometry 병합, InstancedMesh

draw call이 비용인 이유


Mesh를 하나 만들 때마다 하나 이상의 렌더링 요청을 보낸다. 이 요청 하나를 draw call이라고 한다.

물체 1000개를 그린다면 draw call이 1000번 발생한다. 문제는 GPU가 삼각형을 그리는 시간보다 CPU가 요청을 준비하는 시간이 더 오래 걸린다는 점이다. 매번 어떤 셰이더를 쓸지, 어떤 버퍼를 바인딩할지 지정하고 상태가 유효한지 검사해야 하기 때문이다.

그래서 삼각형 100만 개짜리 물체 하나보다, 삼각형 100개짜리 물체 1만 개가 더 느린 상황이 흔하다. 정점 수가 아니라 물체 수가 병목이 되는 것이다.

현재 draw call이 몇 번인지는 렌더러에서 확인할 수 있다.

1renderer.render(scene, camera); 2console.log(renderer.info.render.calls);

geometry 합치기


Mesh를 하나로 합친다면 오버헤드를 줄일 수 있다.

BufferGeometryUtils.mergeGeometries를 쓰면 여러 geometry를 하나로 만들 수 있다. 각 geometry를 원하는 위치로 미리 변환한 뒤 합치는 것이 핵심이다.

1const geometries = []; 2 3for (let i = 0; i < 1000; i++) { 4 const geometry = new THREE.BoxGeometry(1, 1, 1); 5 6 geometry.translate( 7 Math.random() * 100 - 50, 8 Math.random() * 100 - 50, 9 Math.random() * 100 - 50, 10 ); 11 12 geometries.push(geometry); 13} 14 15const merged = BufferGeometryUtils.mergeGeometries(geometries); 16const mesh = new THREE.Mesh(merged, material); 17 18scene.add(mesh); // draw call 1번

한계

합친 뒤에는 하나의 물체가 되므로, 개별 물체를 따로 움직이거나 숨길 수 없다. 색을 다르게 하려면 정점 색상을 써야 하고, 피킹으로 개별 물체를 구분하는 것도 직접 처리해야 한다.

그래서 지형, 건물, 나무처럼 한 번 배치하면 움직이지 않는 것에 적합하다.

InstancedMesh


같은 geometry와 material을 쓰면서 위치만 다른 물체가 많다면 인스턴싱이 더 낫다. 정점 데이터는 한 벌만 GPU에 올리고, 물체별 변환 행렬만 따로 넘긴다.

1const mesh = new THREE.InstancedMesh(geometry, material, 1000); 2const matrix = new THREE.Matrix4(); 3 4for (let i = 0; i < 1000; i++) { 5 matrix.setPosition( 6 Math.random() * 100 - 50, 7 Math.random() * 100 - 50, 8 Math.random() * 100 - 50, 9 ); 10 11 mesh.setMatrixAt(i, matrix); 12} 13 14mesh.instanceMatrix.needsUpdate = true; 15scene.add(mesh);

병합과 달리 개별 인스턴스를 나중에 움직일 수 있다는 것이 장점이다. 색상도 인스턴스별로 줄 수 있다.

1mesh.setColorAt(i, new THREE.Color(0xff0000)); 2mesh.instanceColor.needsUpdate = true;

메모리 면에서도 유리하다. 병합은 정점 데이터를 물체 수만큼 복제하지만, 인스턴싱은 원본 한 벌과 행렬 배열만 가진다.

geometry 병합InstancedMesh
draw call11
정점 메모리물체 수만큼 복제원본 1벌
개별 이동불가능가능
서로 다른 형상가능불가능 (같은 geometry만)

형상이 제각각이면 병합, 같은 형상이 반복되면 인스턴싱이라고 보면 된다.

그 외에 효과가 큰 것들


재질 공유

재질이 다르면 draw call이 나뉜다. 색만 다른 물체가 많다면 재질을 각각 만들지 말고 정점 색상이나 인스턴스 색상을 쓴다.

텍스처 아틀라스

텍스처가 다르면 역시 draw call이 나뉜다. 여러 텍스처를 한 장에 모으고 uv로 구분하면 하나로 묶을 수 있다.

프러스텀 컬링

Three.js는 기본적으로 카메라 밖의 물체를 걸러낸다. 다만 병합이나 인스턴싱을 쓰면 전체가 하나의 물체가 되므로, 일부만 화면에 있어도 전부 그려진다. 장면이 아주 넓다면 영역별로 나눠서 병합하는 편이 낫다.

LOD

멀리 있는 물체는 단순한 형상으로 교체한다. THREE.LOD에 거리별 mesh를 등록해두면 카메라 거리에 따라 자동으로 바뀐다.

1const lod = new THREE.LOD(); 2 3lod.addLevel(highDetailMesh, 0); 4lod.addLevel(midDetailMesh, 50); 5lod.addLevel(lowDetailMesh, 200);

정리


  • 병목은 대개 정점 수가 아니라 draw call 수다. renderer.info.render.calls로 먼저 확인한다.
  • 움직이지 않는 서로 다른 형상 → geometry 병합
  • 같은 형상이 반복되고 개별 제어가 필요 → InstancedMesh
  • 재질과 텍스처를 통일하는 것만으로도 draw call이 크게 줄어드는 경우가 많다.